"""
aplotter is a Python module for drawing graphs in pure ASCII format.
This allows graphs to be included in doc strings (Python programs)
or as illustrations in programs in any computer language.

The module offers a function

plot(x, y, draw_axis=True, plot_slope=True, plot_labels=False, dot='*',
     min_x=None, max_x=None, min_y=None, max_y=None,
     output=sys.stdout)

where x and y are sequences of x and y data for a curve.
Axes are automatically calculated from the x and y data if not
min_x, max_x, min_y or max_y are given.

Multiple curves in the plot is not supported.

Here are examples on various plot commands:

>>> from scitools.aplotter import plot
>>> from numpy import linspace, exp, cos, pi
>>> x = linspace(-2, 2, 81)
>>> y = exp(-0.5*x**2)*cos(pi*x)
>>> plot(x, y)
                                        |
                                       -+1
                                     // |\\
                                    /   |  \
                                   /    |   \
                                  /     |   \
                                  /     |    \
                                 /      |     \
                                /       |      \
                               /        |      \
   -------\                    /        |       \                    -------*
---+-------\\-----------------/---------+--------\-----------------//-------+---
   -2        \                /         |        \                /       +2
              \\              /         |         \             //
                \            /          |         \            /
                 \\         /           |          \         //
                   \       /            |           \       /
                    \    //             |            \-    //
                     ----               -0.63          ---/
                                        |
>>>
>>> plot(x, y, draw_axes=False)

                                       -+1
                                     //  \\
                                    /      \
                                   /        \
                                  /         \
                                  /          \
                                 /            \
                                /              \
                               /               \
   -------\                    /                \                    -------*
   +       \\                 /                  \                 //       +
   -2        \                /                  \                /       +2
              \\              /                   \             //
                \            /                    \            /
                 \\         /                      \         //
                   \       /                        \       /
                    \    //                          \-    //
                     ----               -0.63          ---/

>>>
>>> # plot symbols (the dot argument) at data points:
>>> plot(x, y, plot_slope=False)
                                        |
                                       *+1
                                     ** |**
                                    *   |  *
                                   *    |   *
                                        |
                                  *     |    *
                                 *      |     *
                                *       |      *
                                        |
   ********                    *        |       *                    ********
---+-------**-----------------*---------+--------*-----------------**-------+---
   -2        *                          |                         *       +2
              **              *         |         *             **
                *            *          |         *            *
                 **         *           |          *         **
                   *       *            |           *       *
                    *    **             |            **    **
                     ****               -0.63          ****
                                        |
>>>
>>> # drop axis labels:
>>> plot(x, y, plot_labels=False)
                                        |
                                       -\
                                     // |\\
                                    /   |  \
                                   /    |   \
                                  /     |   \
                                  /     |    \
                                 /      |     \
                                /       |      \
                               /        |      \
   -------\                    /        |       \                    -------*
-----------\\-----------------/---------+--------\-----------------//-----------
             \                /         |        \                /
              \\              /         |         \             //
                \            /          |         \            /
                 \\         /           |          \         //
                   \       /            |           \       /
                    \    //             |            \-    //
                     ----               |              ---/
                                        |
>>>
>>> plot(x, y, dot='o', plot_slope=False)
                                        |
                                       o+1
                                     oo |oo
                                    o   |  o
                                   o    |   o
                                        |
                                  o     |    o
                                 o      |     o
                                o       |      o
                                        |
   oooooooo                    o        |       o                    oooooooo
---+-------oo-----------------o---------+--------o-----------------oo-------+---
   -2        o                          |                         o       +2
              oo              o         |         o             oo
                o            o          |         o            o
                 oo         o           |          o         oo
                   o       o            |           o       o
                    o    oo             |            oo    oo
                     oooo               -0.63          oooo
                                        |
>>>
>>> # store plot in a string:
>>> p = plot(x, y, output=str)
>>> print p
                                        |
                                       -+1
                                     // |\\
                                    /   |  \
                                   /    |   \
                                  /     |   \
                                  /     |    \
                                 /      |     \
                                /       |      \
                               /        |      \
   -------\                    /        |       \                    -------*
---+-------\\-----------------/---------+--------\-----------------//-------+---
   -2        \                /         |        \                /       +2
              \\              /         |         \             //
                \            /          |         \            /
                 \\         /           |          \         //
                   \       /            |           \       /
                    \    //             |            \-    //
                     ----               -0.63          ---/
                                        |
"""
#-----------------------------------------------
#aplotter.py - ascii art function plotter
#Copyright (c) 2006, Imri Goldberg
#All rights reserved.
#Additional documentation and examples by Hans Petter Langtangen.
#
#Redistribution and use in source and binary forms,
#with or without modification, are permitted provided
#that the following conditions are met:
#
#    * Redistributions of source code must retain the
#		above copyright notice, this list of conditions
#		and the following disclaimer.
#    * Redistributions in binary form must reproduce the
#		above copyright notice, this list of conditions
#		and the following disclaimer in the documentation
#		and/or other materials provided with the distribution.
#    * Neither the name of the <ORGANIZATION> nor the names of
#		its contributors may be used to endorse or promote products
#		derived from this software without specific prior written permission.
#
#THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
#AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
#IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
#ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
#LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
#DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
#SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
#CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
#OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
#OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#-----------------------------------------------

import math, sys


EPSILON = 0.000001

def transposed(mat):
	result = []
	for i in range(len(mat[0])):
		result.append([x[i] for x in mat])
	return result

def y_reversed(mat):
	result = []
	for i in range(len(mat)):
		result.append(list(reversed(mat[i])))
	return result

def sign(x):
	if 0<x:
		return 1
	if 0 == x:
		return 0
	return -1

class Plotter(object):

	class PlotData(object):
		def __init__(self, x_size, y_size, min_x, max_x, min_y, max_y, x_mod, y_mod):
			self.x_size = x_size
			self.y_size = y_size
			self.min_x = min_x
			self.max_x = max_x
			self.min_y = min_y
			self.max_y = max_y
			self.x_mod = x_mod
			self.y_mod = y_mod

			self.x_step = float(max_x - min_x)/float(self.x_size)
			self.y_step = float(max_y - min_y)/float(self.y_size)
			self.inv_x_step = 1/self.x_step
			self.inv_y_step = 1/self.y_step

			self.ratio = self.y_step / self.x_step
		def __repr__(self):
			s = "size: %s, bl: %s, tr: %s, step: %s" % ((self.x_size, self.y_size), (self.min_x, self.min_y), (self.max_x, self.max_y),
														 (self.x_step, self.y_step))
			return s

	def __init__(self, **kwargs):

		self.x_size = kwargs.get("x_size", 80)
		self.y_size = kwargs.get("y_size", 20)

		self.will_draw_axes = kwargs.get("draw_axes", True)

		self.new_line = kwargs.get("newline", "\n")

		self.dot = kwargs.get("dot", "*")

		self.plot_slope = kwargs.get("plot_slope", True)

		self.x_margin = kwargs.get("x_margin", 0.05)
		self.y_margin = kwargs.get("y_margin", 0.1)

		self.will_plot_labels = kwargs.get("plot_labels", True)

	@staticmethod
	def get_symbol_by_slope(slope, default_symbol):
		draw_symbol = default_symbol
		if slope > math.tan(3*math.pi/8):
			draw_symbol = "|"
		elif slope > math.tan(math.pi/8) and slope < math.tan(3*math.pi/8):
			draw_symbol = "/"
		elif abs(slope) < math.tan(math.pi/8):
			draw_symbol = "-"
		elif slope < math.tan(-math.pi/8) and slope > math.tan(-3*math.pi/8):
			draw_symbol = "\\"
		elif slope < math.tan(-3*math.pi/8):
			draw_symbol = "|"
		return draw_symbol


	def plot_labels(self, output_buffer, plot_data):
		if plot_data.y_size < 2:
			return

		margin_factor = 1

		do_plot_x_label = True
		do_plot_y_label = True

		x_str = "%+g"
		if plot_data.x_size < 16:
			do_plot_x_label = False
		elif plot_data.x_size < 23:
			x_str = "%+.2g"

		y_str = "%+g"
		if plot_data.x_size < 8:
			do_plot_y_label = False
		elif plot_data.x_size < 11:
			y_str = "%+.2g"

		act_min_x = (plot_data.min_x + plot_data.x_mod*margin_factor)
		act_max_x = (plot_data.max_x - plot_data.x_mod*margin_factor)
		act_min_y = (plot_data.min_y + plot_data.y_mod*margin_factor)
		act_max_y = (plot_data.max_y - plot_data.y_mod*margin_factor)

		if abs(act_min_x) < 1:
			min_x_str = "%+.2g" % act_min_x
		else:
			min_x_str = x_str % act_min_x

		if abs(act_max_x) < 1:
			max_x_str = "%+.2g" % act_max_x
		else:
			max_x_str = x_str % act_max_x

		if abs(act_min_y) < 1:
			min_y_str = "%+.2g" % act_min_y
		else:
			min_y_str = y_str % act_min_y

		if abs(act_max_y) < 1:
			max_y_str = "%+.2g" % act_max_y
		else:
			max_y_str = y_str % act_max_y

		min_x_coord = self.get_coord(act_min_x,plot_data.min_x,plot_data.x_step)
		max_x_coord = self.get_coord(act_max_x,plot_data.min_x,plot_data.x_step)
		min_y_coord = self.get_coord(act_min_y,plot_data.min_y,plot_data.y_step)
		max_y_coord = self.get_coord(act_max_y,plot_data.min_y,plot_data.y_step)


		#print plot_data

		if plot_data.min_y < 0 and plot_data.max_y > 0:
			y_zero_coord = self.get_coord(0, plot_data.min_y, plot_data.y_step)
		else:
			y_zero_coord = self.get_coord(plot_data.min_y, plot_data.min_y, plot_data.y_step)

		if plot_data.min_x < 0 and plot_data.max_x > 0:
			x_zero_coord = self.get_coord(0, plot_data.min_x, plot_data.x_step)
		else:
			x_zero_coord = self.get_coord(plot_data.min_x, plot_data.min_x, plot_data.x_step)

		output_buffer[x_zero_coord][min_y_coord] = "+"
		output_buffer[x_zero_coord][max_y_coord] = "+"
		output_buffer[min_x_coord][y_zero_coord] = "+"
		output_buffer[max_x_coord][y_zero_coord] = "+"

		if do_plot_x_label:

			for i,c in enumerate(min_x_str):
				output_buffer[min_x_coord+i][y_zero_coord-1] = c
			for i,c in enumerate(max_x_str):
				output_buffer[max_x_coord+i-len(max_x_str)][y_zero_coord-1] = c

		if do_plot_y_label:

			for i,c in enumerate(max_y_str):
				output_buffer[x_zero_coord+i][max_y_coord] = c
			for i,c in enumerate(min_y_str):
				output_buffer[x_zero_coord+i][min_y_coord] = c





	def plot_data(self, xy_seq, output_buffer, plot_data):
		if self.plot_slope:
			xy_seq = list(xy_seq)
			#sort according to the x coord
			xy_seq.sort(key = lambda c: c[0])
			prev_p = xy_seq[0]
			e_xy_seq = enumerate(xy_seq)
			e_xy_seq.next()
			for i,(x,y) in e_xy_seq:
				draw_symbol = self.dot
				line_drawn = self.plot_line(prev_p, (x,y), output_buffer, plot_data)
				prev_p = (x,y)
				if not line_drawn:
					if i > 0 and i < len(xy_seq)-1:
						px,py = xy_seq[i-1]
						nx,ny = xy_seq[i+1]

						if abs(nx-px) > EPSILON:
							slope = (1.0/plot_data.ratio)*(ny-py)/(nx-px)
							draw_symbol = self.get_symbol_by_slope(slope, draw_symbol)
					if x < plot_data.min_x or x >= plot_data.max_x or y < plot_data.min_y or y >= plot_data.max_y:
						continue

					x_coord = self.get_coord(x, plot_data.min_x, plot_data.x_step)
					y_coord = self.get_coord(y, plot_data.min_y, plot_data.y_step)
					if x_coord >= 0 and x_coord < len(output_buffer) and y_coord >= 0 and y_coord < len(output_buffer[0]):
						if self.draw_axes:
							if y_coord == self.get_coord(0, plot_data.min_y, plot_data.y_step) and draw_symbol == "-":
								draw_symbol = "="
						output_buffer[x_coord][y_coord] = draw_symbol
		else:
			for x,y in xy_seq:
				if x < plot_data.min_x or x >= plot_data.max_x or y < plot_data.min_y or y >= plot_data.max_y:
					continue
				x_coord = self.get_coord(x, plot_data.min_x, plot_data.x_step)
				y_coord = self.get_coord(y, plot_data.min_y, plot_data.y_step)
				if x_coord >= 0 and x_coord < len(output_buffer) and y_coord > 0 and y_coord < len(output_buffer[0]):
					output_buffer[x_coord][y_coord] = self.dot


	def plot_line(self, start, end, output_buffer, plot_data):

		start_coord = self.get_coord(start[0], plot_data.min_x, plot_data.x_step), self.get_coord(start[1], plot_data.min_y, plot_data.y_step)
		end_coord = self.get_coord(end[0], plot_data.min_x, plot_data.x_step), self.get_coord(end[1], plot_data.min_y, plot_data.y_step)

		x0,y0 = start_coord
		x1,y1 = end_coord
		if (x0,y0) == (x1,y1):
			return True

		clipped_line = clip_line(start, end, (plot_data.min_x, plot_data.min_y), (plot_data.max_x, plot_data.max_y))
		if clipped_line != None:
			start,end = clipped_line
		else:
			return False
		start_coord = self.get_coord(start[0], plot_data.min_x, plot_data.x_step), self.get_coord(start[1], plot_data.min_y, plot_data.y_step)
		end_coord = self.get_coord(end[0], plot_data.min_x, plot_data.x_step), self.get_coord(end[1], plot_data.min_y, plot_data.y_step)

		x0,y0 = start_coord
		x1,y1 = end_coord
		if (x0,y0) == (x1,y1):
			return True
		x_zero_coord = self.get_coord(0, plot_data.min_x, plot_data.x_step)
		y_zero_coord = self.get_coord(0, plot_data.min_y, plot_data.y_step)

		if start[0]-end[0] == 0:
			draw_symbol = "|"
		else:
			slope = (1.0/plot_data.ratio)*(end[1]-start[1])/(end[0]-start[0])
			draw_symbol = self.get_symbol_by_slope(slope, self.dot)
		try:

			delta = x1-x0, y1-y0
			if abs(delta[0])>abs(delta[1]):
				s = sign(delta[0])
				slope = float(delta[1])/delta[0]
				for i in range(0,abs(int(delta[0]))):
					cur_draw_symbol = draw_symbol
					x = i*s
					cur_y = int(y0+slope*x)
					if self.draw_axes and cur_y == y_zero_coord and draw_symbol == "-":
						cur_draw_symbol = "="
					output_buffer[x0+x][cur_y] = cur_draw_symbol


			else:
				s = sign(delta[1])
				slope = float(delta[0])/delta[1]
				for i in range(0,abs(int(delta[1]))):
					y = i*s
					cur_draw_symbol = draw_symbol
					cur_y = y0+y
					if self.draw_axes and cur_y == y_zero_coord and draw_symbol == "-":
						cur_draw_symbol = "="
					output_buffer[int(x0+slope*y)][cur_y] = cur_draw_symbol
		except Exception as e:
			print start, end
			print start_coord, end_coord
			print plot_data
			raise e

		return False


	def plot_single(self, seq, min_x = None, max_x = None, min_y = None, max_y = None):
		return self.plot_double(range(len(seq)),seq, min_x, max_x, min_y, max_y)




	def plot_double(self, x_seq, y_seq, min_x = None, max_x = None, min_y = None, max_y = None):
		if min_x == None:
			min_x = min(x_seq)
		if max_x == None:
			max_x = max(x_seq)
		if min_y == None:
			min_y = min(y_seq)
		if max_y == None:
			max_y = max(y_seq)

		if max_y == min_y:
			max_y += 1

		x_mod = (max_x-min_x)*self.x_margin
		y_mod = (max_y-min_y)*self.y_margin
		min_x-=x_mod
		max_x+=x_mod
		min_y-=y_mod
		max_y+=y_mod


		plot_data = self.PlotData(self.x_size, self.y_size, min_x, max_x, min_y, max_y, x_mod, y_mod)

		output_buffer = [[" "]*self.y_size for i in range(self.x_size)]

		if self.will_draw_axes:
			self.draw_axes(output_buffer, plot_data)

		self.plot_data(zip(x_seq, y_seq), output_buffer, plot_data)

		if self.will_plot_labels:
			self.plot_labels(output_buffer, plot_data)

		trans_result = transposed(y_reversed(output_buffer))

		result = self.new_line.join(["".join(row) for row in trans_result])
		return result

	def draw_axes(self, output_buffer, plot_data):


		draw_x = False
		draw_y = False

		if plot_data.min_x <= 0 and plot_data.max_x > 0:
			draw_y = True
			zero_x = self.get_coord(0, plot_data.min_x, plot_data.x_step)
			for y in range(plot_data.y_size):
				output_buffer[zero_x][y] = "|"

		if plot_data.min_y <= 0 and plot_data.max_y > 0:
			draw_x = True
			zero_y = self.get_coord(0, plot_data.min_y, plot_data.y_step)
			for x in range(plot_data.x_size):
				output_buffer[x][zero_y] = "-"

		if draw_x and draw_y:
			output_buffer[zero_x][zero_y] = "+"


	@staticmethod
	def get_coord(val, min, step):
		result = int((val - min)/step)
		return result

def clip_line(line_pt_1, line_pt_2, rect_bottom_left, rect_top_right):
	ts = [0.0,1.0]
	if line_pt_1[0] == line_pt_2[0]:
		return ((line_pt_1[0], max(min(line_pt_1[1], line_pt_2[1]), rect_bottom_left[1])),
				(line_pt_1[0], min(max(line_pt_1[1], line_pt_2[1]), rect_top_right[1])))
	if line_pt_1[1] == line_pt_2[1]:
		return ((max(min(line_pt_1[0], line_pt_2[0]), rect_bottom_left[0]), line_pt_1[1]),
				(min(max(line_pt_1[0], line_pt_2[0]), rect_top_right[0]), line_pt_1[1]))

	if ((rect_bottom_left[0] <= line_pt_1[0] and line_pt_1[0] < rect_top_right[0]) and
		(rect_bottom_left[1] <= line_pt_1[1] and line_pt_1[1] < rect_top_right[1]) and
		(rect_bottom_left[0] <= line_pt_2[0] and line_pt_2[0] < rect_top_right[0]) and
		(rect_bottom_left[1] <= line_pt_2[1] and line_pt_2[1] < rect_top_right[1])):
		return line_pt_1, line_pt_2

	ts.append( float(rect_bottom_left[0]-line_pt_1[0])/(line_pt_2[0]-line_pt_1[0]) )
	ts.append( float(rect_top_right[0]-line_pt_1[0])/(line_pt_2[0]-line_pt_1[0]) )
	ts.append( float(rect_bottom_left[1]-line_pt_1[1])/(line_pt_2[1]-line_pt_1[1]) )
	ts.append( float(rect_top_right[1]-line_pt_1[1])/(line_pt_2[1]-line_pt_1[1]) )

	ts.sort()
	if ts[2] < 0 or ts[2] >= 1 or ts[3] < 0 or ts[2]>= 1:
		return None
	result = [(pt_1 + t*(pt_2-pt_1)) for t in (ts[2],ts[3]) for (pt_1, pt_2) in zip(line_pt_1, line_pt_2)]
	return (result[0],result[1]), (result[2], result[3])



def plot(*args,**flags):
	limit_flags_names = set(["min_x","min_y","max_x","max_y"])
	limit_flags = dict([(n,flags[n]) for n in limit_flags_names & set(flags)])
	settting_flags = dict([(n,flags[n]) for n in set(flags) - limit_flags_names])
	output = flags.get('output', sys.stdout)
	if len(args) == 1:
		p = Plotter(**settting_flags)
		r = p.plot_single(args[0],**limit_flags)
	elif len(args) == 2:
		p = Plotter(**settting_flags)
		r = p.plot_double(args[0],args[1],**limit_flags)
	else:
		raise NotImplementedError("can't draw multiple graphs yet")
	if output == sys.stdout:
		print r
	else:
		return r

__all__ = ["Plotter","plot"]

def _demo():
	from numpy import linspace, exp, sin, pi
	x = linspace(-2, 2, 31)
	y = exp(-0.5*x**2)*sin(2*pi*x)
	data = """
from numpy import linspace, exp, sin, pi
x = linspace(-2, 2, 31)
y = exp(-0.5*x**2)*sin(2*pi*x)
"""
	cmd = ["plot(x, y)",
	       "plot(x, y, draw_axes=False)",
	       "plot(x, y, plot_slope=False)",
	       "plot(x, y, plot_labels=False)",
	       "plot(x, y, dot='o', plot_slope=False)",
	       "p = plot(x, y, output=str)",
	       "print p"
	       ]
	print data
	for c in cmd:
		print '\n\n', c
	        exec(c)

if __name__ == '__main__':
	_demo()
